Skip to content

Major lljson rework to make round-trippable serialization easier#61

Open
HaroldCindy wants to merge 11 commits intomainfrom
harold/add_json_replacer
Open

Major lljson rework to make round-trippable serialization easier#61
HaroldCindy wants to merge 11 commits intomainfrom
harold/add_json_replacer

Conversation

@HaroldCindy
Copy link
Contributor

Of note, there's now a notion of replacer and reviver callbacks as in JS' JSON APIs. An example of their use is in the tests folder as lljson_typedjson.lua.

We went with this form since it allows constructing a different representation of the object before serializing without requiring you to construct an entire, serializable copy before calling lljson.encode(). That allows you to save memory, since the serializable version of each object only need to be alive as long as we're still traversing the object.

Additionally, an empty table is now encoded as [] by default. This is probably the most common meaning for an empty table, but you can also apply object_mt as a metatable or add __jsontype="object" to your own metatable to force serialization as an object. Similarly, array_mt or __jsontype="array" will act as a hint to treat your object as an array.

__len should no longer be used as a hint that the object should be treated as an array, that's what __jsontype is for.

Also added a new options table format to lljson.encode() and friends. The table now allows you to specify that __tojson hooks should be skipped, so you can manually invoke them at your leisure in your replacer hooks.

Of note, there's now a notion of replacer and reviver callbacks as in
JS' `JSON` APIs. An example of their use is in the tests folder as
`lljson_typedjson.lua`.

We went with this form since it allows constructing a different representation
of the object before serializing without requiring you to construct an entire,
serializable copy before calling `lljson.encode()`. That allows you to save memory,
since the serializable version of each object only need to be alive as long
as we're still traversing the object.

Additionally, an empty table is now encoded as `[]` by default. This is
probably the most common meaning for an empty table, but you can also
apply `object_mt` as a metatable or add `__jsontype="object"` to your
own metatable to force serialization as an object. Similarly, `array_mt`
or `__jsontype="array"` will act as a hint to treat your object as an array.

`__len` should no longer be used as a hint that the object should be treated
as an array, that's what `__jsontype` is for.

Also added a new options table format to `lljson.encode()` and friends. The
table now allows you to specify that `__tojson` hooks should be skipped, so
you can manually invoke them at your leisure in your replacer hooks.
@HaroldCindy HaroldCindy marked this pull request as ready for review March 16, 2026 23:08
@@ -1 +1 @@
/* Lua CJSON - JSON support for Lua
Copy link

@Suzanna-Linn Suzanna-Linn Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm learning how it works, is it like this? in the most metatably case?

table1
if replacer then
table2 = table1.__tojson
table3 = replacer
end
shape = table3.__jsontype
table4 = table3.__tojson
if array then
len = table4.__len
end

  • A table can be replaced 3 times (__tojson called in replacer, the replacer and __tojson in serialization).
  • If replacer returns the same table, its __tojson is called twice (in replacer and in serialization).
  • In serialization, __tojson returning a table with __jsontype has no effect.

Copy link

@tapple tapple Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like you are correct. __tojson can be called both before and after the replacer:

> tojson = { __tojson = function(self, ...) print("tojson", self, ...); return self end }
> function replacer(...) print("replacer", ...); return setmetatable({}, tojson) end
> lljson.encode(setmetatable({}, tojson), {replacer=replacer})
tojson  table: 0x00000000398548a0       table: 0x0000000039854830
replacer        nil     table: 0x00000000398548a0       nil
tojson  table: 0x0000000039854788       table: 0x0000000039854830
[]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like __jsontype is indeed ignored on tables returned from __tojson:

> lljson.encode(setmetatable({}, {__jsontype="object"}))
{}
> lljson.encode(setmetatable({}, {__tojson = function() return setmetatable({}, {__jsontype="object"}) end }))
[]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, __jsontype is what determines the encoding semantic for the object, __tojson() is a hook that objects can use to present their own view of the data to the serializer. Will look at that double __tojson call though.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding __jsontype and __tojson, a theoretical (and probably non-existent case) would be a __tojson that returns, depending on the data, any one of:

  • a JSON object
  • a JSON array
  • an SLua array to be encoded as a JSON object

Then, we would need to set __jsontype in __tojson and use a replacer that does nothing ( replacer = function(_, v) return v end ) to get __tojson called and __jsontype set before it is used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, fixed the double __tojson call in the replacer case, __tojson should only ever be called once, and before the replacer as is the case in JSON.stringify() in JS. Still not entirely sure what to do with __jsontype though...

HaroldCindy and others added 4 commits March 19, 2026 09:26
Co-authored-by: Tapple Gao <tapplek@gmail.com>
This helps improve round-trippability of JSON payloads from outside SL,
preserving the `array`-ness or `object`-ness of empty tables especially.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants